package org.smartboot.flow.spring.extension;

import org.smartboot.flow.core.attribute.AttributeHolder;
import org.smartboot.flow.core.attribute.AttributeValueResolver;
import org.smartboot.flow.core.attribute.Attributes;
import org.smartboot.flow.core.parser.DefinitionVisitor;
import org.smartboot.flow.core.parser.ParserContext;
import org.smartboot.flow.core.parser.definition.AdapterDefinition;
import org.smartboot.flow.core.parser.definition.ChooseDefinition;
import org.smartboot.flow.core.parser.definition.ElementDefinition;
import org.smartboot.flow.core.parser.definition.EngineDefinition;
import org.smartboot.flow.core.parser.definition.IfElementDefinition;
import org.smartboot.flow.core.parser.definition.PipelineComponentDefinition;
import org.smartboot.flow.core.parser.definition.PipelineDefinition;
import org.smartboot.flow.core.parser.definition.ScriptDefinition;
import org.smartboot.flow.core.script.ScriptCondition;
import org.smartboot.flow.core.util.AssertUtil;
import org.smartboot.flow.core.util.AuxiliaryUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;

import java.util.List;

/**
 * @author qinluo
 * @date 2022/11/17 0:01
 * @since 1.0.0
 */
public class BeanDefinitionVisitor implements DefinitionVisitor, BeanFactoryAware {

    private ParserContext context;

    /**
     * Temporary def register.
     */
    private BeanDefinitionRegister register;

    public BeanDefinitionVisitor() {
    }

    public BeanDefinitionVisitor(BeanDefinitionRegistry registry, ParserContext context) {
        AssertUtil.notNull(registry, "registry must not be null!");
        this.context = context;
        this.register = new BeanDefinitionRegister(registry);
    }

    @Override
    public void init(ParserContext context) {
        this.context = context;
        AssertUtil.notNull(register, "registry must not be null!");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (beanFactory instanceof BeanDefinitionRegistry) {
            this.register = new BeanDefinitionRegister((BeanDefinitionRegistry) beanFactory);
        }
    }

    @Override
    public void visit(ElementDefinition ed) {
        if (ed instanceof ScriptDefinition) {
            ScriptDefinition sed = (ScriptDefinition)ed;
            Class<?> javaType = sed.getJavaType();
            // Maybe a ref.
            if (javaType == null) {
                BeanDefinition beanDefinition = register.getBeanDefinition(sed.getName());
                String beanClassName = beanDefinition.getBeanClassName();
                Class<?> defJavaType = AuxiliaryUtils.asClass(beanClassName);
                AssertUtil.notNull(defJavaType, "bean " + sed.getName() + " javaType must be subclass of ScriptCondition");
                AssertUtil.isTrue(ScriptCondition.class.isAssignableFrom(defJavaType), "bean " + sed.getName() + " javaType must be subclass of ScriptCondition");

                // Register script content.
                beanDefinition.getPropertyValues().add("script", sed.getScript());

                if (AuxiliaryUtils.isNotBlank(sed.getName())) {
                    beanDefinition.getPropertyValues().add("name", sed.getName());
                }

            } else {
                RootBeanDefinition definition = new RootBeanDefinition();
                definition.setBeanClass(javaType);
                PropertyValue script = new PropertyValue("script", sed.getScript());
                PropertyValue name = new PropertyValue("name", sed.getName());
                definition.getPropertyValues().addPropertyValue(name);
                definition.getPropertyValues().addPropertyValue(script);
                register.registerBeanDefinition(ed.getIdentifier(), definition, true);
            }

        } else {
            ed.visit(this);
        }
    }

    @Override
    public void visit(EngineDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());
        PropertyValue pipeline = new PropertyValue("pipeline", new RuntimeBeanReference(ed.getPipeline()));

        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);
        definition.getPropertyValues().addPropertyValue(pipeline);
        definition.setInitMethodName("validate");
        register.registerBeanDefinition(ed.getIdentifier(), definition, true);
    }

    @Override
    public void visit(PipelineDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());

        ManagedList<Object> components = new ManagedList<>();
        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);
        definition.getPropertyValues().add("components", components);

        ed.getChildren().forEach(p -> p.visit(this));

        for (ElementDefinition elementDef : ed.getChildren()) {
            // Fire sub visit.
            elementDef.visit(this);

            BeanDefinition beanDefinition = register.getBeanDefinition(elementDef.getIdentifier());
            AssertUtil.notNull(beanDefinition, elementDef.getIdentifier() + " not exist");
            components.add(beanDefinition);
        }

        register.registerBeanDefinition(ed.getIdentifier(), definition, !isAnonymous(ed.getIdentifier()));
    }

    @Override
    public void visit(PipelineComponentDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());
        PropertyValue pipeline = new PropertyValue("pipeline", new RuntimeBeanReference(ed.getPipeline()));

        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);

        if (isAnonymous(ed.getPipeline())) {
            context.getRegistered(ed.getPipeline()).visit(this);
            definition.getPropertyValues().add("pipeline", register.getBeanDefinition(ed.getPipeline()));
        } else {
            definition.getPropertyValues().addPropertyValue(pipeline);
        }

        appendAttributes(ed, definition);
        register.registerBeanDefinition(ed.getIdentifier(), definition);
    }

    private boolean isAnonymous(String name) {
        return name.contains("anonymous-pipeline");
    }

    private void appendAttributes(ElementDefinition ed, RootBeanDefinition definition) {
        List<AttributeHolder> attributes = ed.getAttributes();
        AttributeValueResolver resolver = AttributeValueResolver.getInstance();

        for (AttributeHolder holder : attributes) {
            Attributes attribute = holder.getAttribute();

            try {
                Object resolved = resolver.resolve(attribute, holder.getValue());
                holder.setValue(resolved);
            } catch (Exception ignored) {

            }

            if (attribute == Attributes.NAME || attribute == Attributes.DEGRADE_CALLBACK) {
                continue;
            }
            PropertyValue value = new PropertyValue(attribute.getName(), holder.getValue());
            definition.getPropertyValues().addPropertyValue(value);


        }

        definition.getPropertyValues().add("attributes", ed.getAttributes());
    }

    @Override
    public void visitBasic(ElementDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());

        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);

        if (isType(ed.getExecute())) {
            RootBeanDefinition conditionDef = new RootBeanDefinition();
            conditionDef.setBeanClassName(ed.getExecute());
            definition.getPropertyValues().add("executable", conditionDef);
        } else {
            AssertUtil.notBlank(ed.getExecute(), "component element ref must not be null!");
            definition.getPropertyValues().add("executable", new RuntimeBeanReference(ed.getExecute()));
        }

        AttributeHolder degradeCallback = ed.getAttributes().stream()
                .filter(p -> p.getAttribute() == Attributes.DEGRADE_CALLBACK).findFirst().orElse(null);

        if (degradeCallback != null) {
            String callback = String.valueOf(degradeCallback.getValue());

            if (isType(callback)) {
                RootBeanDefinition conditionDef = new RootBeanDefinition();
                conditionDef.setBeanClassName(callback);
                definition.getPropertyValues().add("degradeCallback", conditionDef);
            } else {
                AssertUtil.notBlank(callback, "component callback ref must not be null!");
                definition.getPropertyValues().add("degradeCallback", new RuntimeBeanReference(callback));
            }
        }

        appendAttributes(ed, definition);
        register.registerBeanDefinition(ed.getIdentifier(), definition);
    }

    private boolean isType(String type) {
        try {
            Class.forName(type, false, this.getClass().getClassLoader());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public void visit(IfElementDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());

        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);

        if (isType(ed.getTest())) {
            RootBeanDefinition conditionDef = new RootBeanDefinition();
            conditionDef.setBeanClassName(ed.getTest());
            definition.getPropertyValues().add("condition", conditionDef);
        } else {
            AssertUtil.notBlank(ed.getTest(), "if element ref must not be null!");
            definition.getPropertyValues().add("condition", new RuntimeBeanReference(ed.getTest()));
        }

        ed.getIfThenRef().visit(this);
        BeanDefinition beanDefinition = register.getBeanDefinition(ed.getIfThenRef().getIdentifier());
        AssertUtil.notNull(beanDefinition, ed.getIfThenRef().getIdentifier() + " not exist");
        definition.getPropertyValues().add("thenComponent", beanDefinition);

        if (ed.getIfElseRef() != null) {
            ed.getIfElseRef().visit(this);
            beanDefinition = register.getBeanDefinition(ed.getIfElseRef().getIdentifier());
            AssertUtil.notNull(beanDefinition, ed.getIfElseRef().getIdentifier() + " not exist");
            definition.getPropertyValues().add("elseComponent", beanDefinition);
        }

        appendAttributes(ed, definition);
        register.registerBeanDefinition(ed.getIdentifier(), definition);
    }

    @Override
    public void visit(ChooseDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());

        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);
        definition.getPropertyValues().add("allBranchWasString", true);

        if (isType(ed.getTest())) {
            RootBeanDefinition conditionDef = new RootBeanDefinition();
            conditionDef.setBeanClassName(ed.getTest());
            definition.getPropertyValues().add("condition", conditionDef);
        } else {
            definition.getPropertyValues().add("condition", new RuntimeBeanReference(ed.getTest()));
        }

        ManagedMap<String, Object> managedMap = new ManagedMap<>();
        List<ElementDefinition> chooseCaseList = ed.getChooseCaseList();
        for (ElementDefinition caseDef : chooseCaseList) {
            caseDef.visit(this);
            BeanDefinition beanDefinition = register.getBeanDefinition(caseDef.getIdentifier());
            AssertUtil.notNull(beanDefinition, "case " + caseDef.getWhen() + " " + ed.getChooseDefaultRef().getIdentifier() + " not exist");
            managedMap.put(caseDef.getWhen(), beanDefinition);
        }

        definition.getPropertyValues().add("branches", managedMap);

        if (ed.getChooseDefaultRef() != null) {
            ed.getChooseDefaultRef().visit(this);
            BeanDefinition beanDefinition = register.getBeanDefinition(ed.getChooseDefaultRef().getIdentifier());
            AssertUtil.notNull(beanDefinition, ed.getChooseDefaultRef().getIdentifier() + " not exist");
            definition.getPropertyValues().add("defaultBranch", beanDefinition);
        }

        appendAttributes(ed, definition);
        register.registerBeanDefinition(ed.getIdentifier(), definition);
    }

    @Override
    public void visit(AdapterDefinition ed) {
        RootBeanDefinition definition = new RootBeanDefinition();
        PropertyValue name = new PropertyValue("name", ed.getName());

        definition.setBeanClass(ed.resolveType());
        definition.getPropertyValues().addPropertyValue(name);

        if (isType(ed.getExecute())) {
            RootBeanDefinition adapterDef = new RootBeanDefinition();
            adapterDef.setBeanClassName(ed.getExecute());
            definition.getPropertyValues().add("adapter", adapterDef);
        } else {
            AssertUtil.notNull(ed.getExecute(), "adapter[execute] must not be null");
            definition.getPropertyValues().add("adapter", new RuntimeBeanReference(ed.getExecute()));
        }

        ed.getPipelineElement().visit(this);

        BeanDefinition beanDefinition = register.getBeanDefinition(ed.getPipelineElement().getIdentifier());
        definition.getPropertyValues().add("component", beanDefinition);

        register.registerBeanDefinition(ed.getIdentifier(), definition);

    }
}
